package com.kamistoat.meimeimall.common.sharding.interceptor;

import com.google.common.collect.Maps;
import com.kamistoat.meimeimall.common.exception.BizCodeEnum;
import com.kamistoat.meimeimall.common.exception.RRException;
import com.kamistoat.meimeimall.common.sharding.Sharding;
import com.kamistoat.meimeimall.common.sharding.ShardingContext;
import com.kamistoat.meimeimall.common.sharding.strategy.DefaultShardingStrategy;
import com.kamistoat.meimeimall.common.sharding.strategy.ShardingStrategy;
import org.apache.ibatis.binding.MapperMethod;
import org.apache.ibatis.executor.statement.StatementHandler;
import org.apache.ibatis.plugin.*;
import org.apache.ibatis.reflection.MetaObject;
import org.apache.ibatis.reflection.SystemMetaObject;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;

import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.sql.Connection;
import java.util.Map;
import java.util.Properties;

/**
 * Mybatis的拦截器类。
 * 使用 @Intercepts 声明拦截器。
 * 使用 @Signature 声明拦截器拦截的类(type)、方法名(method)、方法输入参数(args)
 */
@Component
@Intercepts({@Signature(type = StatementHandler.class, method = "prepare", args = {Connection.class, Integer.class})})
public class ShardingInterceptor implements Interceptor {

    private final static Logger logger = LoggerFactory.getLogger(ShardingInterceptor.class);
    /**
     * StatementHandler 类内属性常量名称。只有将其转换为 MetaObject 才能直接这么访问。
     */
    private static final String DELEGATE_BOUND_SQL_SQL = "delegate.boundSql.sql";
    private static final String DELEGATE_MAPPED_STATEMENT_ID = "delegate.mappedStatement.id";
    private static final String DELEGATE_PARAMETER_HANDLER_PARAMETER_OBJECT = "delegate.parameterHandler.parameterObject";
    private static final String POINT = ".";

    private static final ShardingStrategy DEFAULT_SHARDING_STRATEGY = new DefaultShardingStrategy();
    /**
     * 策略Map，目的是控制策略实例的数量，不让每次分库分表都创建实例
     */
    private static final Map<String, ShardingStrategy> SHARDING_STRATEGY_MAP = Maps.newConcurrentMap();
    /**
     * 注解Map，目的是快速寻找被拦截方法上的注解，不用每次都去反射寻找
     */
    private static final Map<String, Sharding> SHARDING_ANNOTATION_MAP = Maps.newConcurrentMap();


    /**
     * Mybatis拦截器覆写方法
     *
     * @param invocation 拦截器
     * @return 下游
     * @throws Throwable
     */
    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        /**
         * Invocation 是Mybatis拦截器的内部类。
         * Invocation.getTarget() 获取被拦截的类，其实就是 @Signature 中声明的 type
         */
        StatementHandler statementHandler = (StatementHandler) realTarget(invocation.getTarget());
        // MetaObject 是 mybatis 的一种内部属性类，会将给定类实例中的所有属性做成Map。此处将拦截的 StatementHandler 做成了Map。
        // MetaObject会将给定类的属性名以 . 链接嵌套做Key，之所以做成 MetaObject， 是为了快速访问类内属性，省去不停的使用get()方法
        MetaObject metaObject = SystemMetaObject.forObject(statementHandler);

        // 提取 statementHandler 的 mapperId，标识拦截的Dao方法，找到该方法的 @Sharding注解
        String methodName = (String) metaObject.getValue(DELEGATE_MAPPED_STATEMENT_ID);
        Sharding sharding = SHARDING_ANNOTATION_MAP.get(methodName);
        if (sharding != null) {
            String className = methodName.substring(0, methodName.lastIndexOf(POINT));
            Method[] methods = Class.forName(className).getMethods();
            for (Method method : methods) {
                if (method.getName().equals(methodName.substring(methodName.lastIndexOf(POINT)))) {
                    SHARDING_ANNOTATION_MAP.put(methodName, method.getAnnotation(Sharding.class));
                    sharding = SHARDING_ANNOTATION_MAP.get(methodName);
                    break;
                }
            }
        }

        // 如果@Sharding注解开启，则进入分库分表的逻辑
        if (sharding != null && sharding.sharding()) {
            if (StringUtils.isEmpty(sharding.shardingKey()) || StringUtils.isEmpty(sharding.databaseName()) ||
                    StringUtils.isEmpty(sharding.tableName())) {
                throw new RRException(BizCodeEnum.SHARDING_ANNOTATION_ERROR.getMessage(), BizCodeEnum.SHARDING_ANNOTATION_ERROR.getCode());
            }
            // 获取分库分表key的真实值
            String shardingKeyValue = getShardingKeyValue(sharding, metaObject);
            if (StringUtils.isEmpty(shardingKeyValue)) {
                throw new RRException(BizCodeEnum.SHARDING_PARAM_WITHOUT_ALLY.getMessage(), BizCodeEnum.SHARDING_PARAM_WITHOUT_ALLY.getCode());
            }
            // 获取Mybatis构造的SQL
            String sql = (String) metaObject.getValue(DELEGATE_BOUND_SQL_SQL);
            // 将SQL中所有的原始表名，替换成分库分表后的表名。注意此处只替换表名，至于库，那是数据源链接做的事情
            sql = sql.replaceAll(sharding.tableName(), getTargetTableName(sharding, shardingKeyValue));
            // 写回拦截器
            metaObject.setValue(DELEGATE_BOUND_SQL_SQL, sql);
        }
        // 放行
        Object proceed = invocation.proceed();
        /**
         * 清空上下文
         */
        ShardingContext.clear();
        return proceed;
    }

    /**
     * 如果是多重代理，则需要一直向下找到真正的 StatementHandler
     */
    private Object realTarget(Object target) {
        if (Proxy.isProxyClass(target.getClass())) {
            MetaObject metaObject = SystemMetaObject.forObject(target);
            return realTarget(metaObject.getValue("h.target"));
        }
        return target;
    }

    /**
     * 根据 @Sharding 注解的标识，找出分库分表Key的真实值，转成String返回。
     *
     * @param sharding   @Sharding注解
     * @param metaObject statementHandler做成的元数据
     * @return
     */
    private String getShardingKeyValue(Sharding sharding, MetaObject metaObject) {
        // 利用 MetaObject 元数据特性，直接用嵌套属性名提取属性。
        Object parameterObject = metaObject.getValue(DELEGATE_PARAMETER_HANDLER_PARAMETER_OBJECT);
        // 如果Dao方法只有一个输入参数，则 parameterObject 会直接以对应类型展示
        if (!(parameterObject instanceof MapperMethod.ParamMap)) {
            throw new RRException(BizCodeEnum.SHARDING_PARAM_WITHOUT_ALLY.getMessage(), BizCodeEnum.SHARDING_PARAM_WITHOUT_ALLY.getCode());
        } else {
            Map<String, Object> parameterMap = (Map<String, Object>) parameterObject;
            Object shardingKeyValue = parameterMap.get(sharding.shardingKey());
            if (shardingKeyValue != null) {
                return (String) shardingKeyValue;
            }
        }
        return null;
    }

    /**
     * 获取分库分表后的具体表名，用来替换基础表名。注意表名不含数据源信息
     *
     * @param sharding         @Sharding注解实例
     * @param shardingKeyValue 分库分表Key在当前方法中的确切值
     * @return 分库分表后的表名
     * @throws Exception
     */
    private String getTargetTableName(Sharding sharding, String shardingKeyValue) throws Exception {
        // 获取作为分库分表的字段名. 上游判断确保此处是非空
        return getShardingStrategy(sharding).constructConnectAndReturnTableName(shardingKeyValue, sharding);
    }

    /**
     * 获取分库分表的策略。若未指定则使用默认策略
     *
     * @param sharding @Sharding注解
     * @return
     * @throws Exception
     */
    private ShardingStrategy getShardingStrategy(Sharding sharding) throws Exception {
        Class<? extends ShardingStrategy> shardingStrategyClass = sharding.strategy();
        ShardingStrategy shardingStrategy = SHARDING_STRATEGY_MAP.get(shardingStrategyClass.getName());
        if (shardingStrategy == null) {
            ShardingStrategy strategy = shardingStrategyClass.newInstance();
            SHARDING_STRATEGY_MAP.putIfAbsent(shardingStrategyClass.getName(), strategy);
            shardingStrategy = SHARDING_STRATEGY_MAP.get(shardingStrategyClass.getName());
        }
        return shardingStrategy;
    }

    @Override
    public Object plugin(Object target) {
        if (target instanceof StatementHandler) {
            return Plugin.wrap(target, this);
        }
        return target;
    }

    @Override
    public void setProperties(Properties properties) {

    }
}
